// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Concurrent;
using JetBrains.Annotations;

namespace SnapObjects.Data
{
    /// <summary>
    ///     <para>
    ///         Maps string property types to their corresponding relational database types.
    ///     </para>
    ///     <para>
    ///         This type is typically used by database providers (and other extensions). It is generally
    ///         not used in application code.
    ///     </para>
    /// </summary>
    public class StringTypeMapper : IStringTypeMapper
    {
        private readonly ConcurrentDictionary<int, TypeMapping> _boundedAnsiMappings
            = new ConcurrentDictionary<int, TypeMapping>();

        private readonly ConcurrentDictionary<int, TypeMapping> _boundedUnicodeMappings
            = new ConcurrentDictionary<int, TypeMapping>();

        /// <summary>
        ///     Initializes a new instance of the <see cref="StringRelationalTypeMapper" /> class.
        /// </summary>
        /// <param name="maxBoundedAnsiLength"> The maximum length of a bounded ANSI string. </param>
        /// <param name="defaultAnsiMapping"> The default mapping of an ANSI string. </param>
        /// <param name="unboundedAnsiMapping"> The mapping for an unbounded ANSI string. </param>
        /// <param name="keyAnsiMapping"> The mapping for an ANSI string that is part of a key. </param>
        /// <param name="createBoundedAnsiMapping"> The function to create a mapping for a bounded ANSI string. </param>
        /// <param name="maxBoundedUnicodeLength"> The maximum length of a bounded Unicode string. </param>
        /// <param name="defaultUnicodeMapping"> The default mapping of a Unicode string. </param>
        /// <param name="unboundedUnicodeMapping"> The mapping for an unbounded Unicode string. </param>
        /// <param name="keyUnicodeMapping"> The mapping for a Unicode string that is part of a key. </param>
        /// <param name="createBoundedUnicodeMapping"> The function to create a mapping for a bounded Unicode string. </param>
        public StringTypeMapper(
            int maxBoundedAnsiLength,
            [NotNull] TypeMapping defaultAnsiMapping,
            [CanBeNull] TypeMapping unboundedAnsiMapping,
            [CanBeNull] TypeMapping keyAnsiMapping,
            [NotNull] Func<int, TypeMapping> createBoundedAnsiMapping,
            int maxBoundedUnicodeLength,
            [NotNull] TypeMapping defaultUnicodeMapping,
            [CanBeNull] TypeMapping unboundedUnicodeMapping,
            [CanBeNull] TypeMapping keyUnicodeMapping,
            [NotNull] Func<int, TypeMapping> createBoundedUnicodeMapping)
        {
            this.MaxBoundedAnsiLength = maxBoundedAnsiLength;
            this.DefaultAnsiMapping = defaultAnsiMapping;
            this.UnboundedAnsiMapping = unboundedAnsiMapping;
            this.KeyAnsiMapping = keyAnsiMapping;
            this.CreateBoundedAnsiMapping = createBoundedAnsiMapping;

            this.MaxBoundedUnicodeLength = maxBoundedUnicodeLength;
            this.DefaultUnicodeMapping = defaultUnicodeMapping;
            this.UnboundedUnicodeMapping = unboundedUnicodeMapping;
            this.KeyUnicodeMapping = keyUnicodeMapping;
            this.CreateBoundedUnicodeMapping = createBoundedUnicodeMapping;
        }

        /// <summary>
        ///     Gets the maximum length of a bounded ANSI string.
        /// </summary>
        public virtual int MaxBoundedAnsiLength { get; }

        /// <summary>
        ///     Gets the default mapping of an ANSI string.
        /// </summary>
        public virtual TypeMapping DefaultAnsiMapping { get; }

        /// <summary>
        ///     Gets the mapping for an unbounded ANSI string.
        /// </summary>
        public virtual TypeMapping UnboundedAnsiMapping { get; }

        /// <summary>
        ///     Gets the mapping for an ANSI string that is part of a key.
        /// </summary>
        public virtual TypeMapping KeyAnsiMapping { get; }

        /// <summary>
        ///     Gets the function to create a mapping for a bounded ANSI string.
        /// </summary>
        public virtual Func<int, TypeMapping> CreateBoundedAnsiMapping { get; }

        /// <summary>
        ///     Gets the maximum length of a bounded Unicode string.
        /// </summary>
        public virtual int MaxBoundedUnicodeLength { get; }

        /// <summary>
        ///     Gets the default mapping of a Unicode string.
        /// </summary>
        public virtual TypeMapping DefaultUnicodeMapping { get; }

        /// <summary>
        ///     Gets the mapping for an unbounded Unicode string.
        /// </summary>
        public virtual TypeMapping UnboundedUnicodeMapping { get; }

        /// <summary>
        ///     Gets the mapping for a Unicode string that is part of a key.
        /// </summary>
        public virtual TypeMapping KeyUnicodeMapping { get; }

        /// <summary>
        ///     Gets the function to create a mapping for a bounded Unicode string.
        /// </summary>
        public virtual Func<int, TypeMapping> CreateBoundedUnicodeMapping { get; }

        /// <summary>
        ///     Gets the relational database type for a string property.
        /// </summary>
        /// <param name="unicode">A value indicating whether the property should handle Unicode data or not.</param>
        /// <param name="keyOrIndex">A value indicating whether the property is part of a key or not.</param>
        /// <param name="maxLength">The maximum length of data the property is configured to store, or null if no maximum is configured.</param>
        /// <returns>
        ///     The type mapping to be used.
        /// </returns>
        public virtual TypeMapping FindMapping(bool unicode, bool keyOrIndex, int? maxLength)
        {
            return unicode
                           ? FindMapping(
                               keyOrIndex,
                               maxLength,
                               this.MaxBoundedUnicodeLength,
                               this.UnboundedUnicodeMapping,
                               this.DefaultUnicodeMapping,
                               this.KeyUnicodeMapping,
                               _boundedUnicodeMappings,
                               this.CreateBoundedUnicodeMapping)
                           : FindMapping(
                               keyOrIndex,
                               maxLength,
                               this.MaxBoundedAnsiLength,
                               this.UnboundedAnsiMapping,
                               this.DefaultAnsiMapping,
                               this.KeyAnsiMapping,
                               _boundedAnsiMappings,
                               this.CreateBoundedAnsiMapping);
        }

        private static TypeMapping FindMapping(
            bool isKeyOrIndex,
            int? maxLength,
            int maxBoundedLength,
            TypeMapping unboundedMapping,
            TypeMapping defaultMapping,
            TypeMapping keyMapping,
            ConcurrentDictionary<int, TypeMapping> boundedMappings,
            Func<int, TypeMapping> createBoundedMapping)
        {
            var mapping = isKeyOrIndex ? keyMapping : defaultMapping;

            if (maxLength.HasValue
                && maxLength != mapping.Size)
            {
                return maxLength <= maxBoundedLength
                    ? boundedMappings.GetOrAdd(maxLength.Value, createBoundedMapping)
                    : unboundedMapping;
            }

            return mapping;
        }
    }
}
